/*
 * This file is part of Catfish.
 * Copyright (C) 2010 Namazu Studios LLC
 * 
 * Catfish is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of 
 * the License, or (at your option) any later version.
 * 
 * Catfish is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with Catfish.  If not, please visit:
 *  
 * http://www.gnu.org/licenses/
 *  
 */
package com.namazustudios.catfish.gae;

import static com.namazustudios.catfish.gae.GaeCatfish.entity;
import static com.namazustudios.catfish.gae.GaeCatfish.key;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.inject.Inject;
import com.namazustudios.catfish.CatfishException;
import com.namazustudios.catfish.Unknown;
import com.namazustudios.catfish.Unmarshaller;
import com.namazustudios.catfish.annotation.Child;
import com.namazustudios.catfish.annotation.Flat;
import com.namazustudios.catfish.annotation.Property;
import com.namazustudios.catfish.converter.Converter;
import com.namazustudios.catfish.provider.ValueProvider;

public class GaeUnmarshaller implements Unmarshaller {

    @Inject(optional = true)
    private Logger logger = Logger.getLogger(GaeUnmarshaller.class.getName());

    @Inject
    private GaeCatfish catfish;

    @Inject
    private MemcacheService memcache;

    @Inject(optional = true)
    private Map<String, Converter<?, ?>> converters = Collections.emptyMap();

    @Inject(optional = true)
    private Map<String, ValueProvider<?>> valueProviders = Collections.emptyMap();

    private Map<Key, Object> objects = new HashMap<Key, Object>();

    private Map<Key, Entity> entities = new HashMap<Key, Entity>();

    private Transaction transaction;

    @Override
    public Object unmarshall(Entity entity) {
    	return unmarshall(null, entity);
    }

    @Override
    public Object unmarshall(Transaction transaction, Entity entity) {
    	this.transaction = transaction;
        Object object = catfish.instantiate(entity.getKey());

        try {
            Map<Object, Object> objects;

            this.objects.clear();
            this.entities.clear();

            doUnmarshall(object, entity);

            objects = new HashMap<Object, Object>();

            for (Entry<Key, Object> entry : this.objects.entrySet()) {
                Key k = entry.getKey();
                Object o = entry.getValue();
                Entity e = entities.get(entry.getKey());

                catfish.firePostGet(o.getClass(), o, e);

                if (catfish.isCacheable(entry.getValue())) {
                    objects.put(k, o);
                }

            }

            memcache.putAll(objects);

            return object;

        } finally {
            this.objects.clear();
            this.entities.clear();
        }
    }

    private void doUnmarshall(Object object, Entity entity) {
        Class<?> cls = object.getClass();

        catfish.check(object, entity);

        do {
            final Key key = entity.getKey();

            for (Field field : cls.getDeclaredFields()) {
                boolean accessable = field.isAccessible();

                field.setAccessible(true);

                try {
                    if (field.isAnnotationPresent(key())) {
                        unmarshallKey(entity, object, field);
                    } else if (field.isAnnotationPresent(Property.class)) {
                        unmarshallProperty(entity, object, field);
                    } else if (field.isAnnotationPresent(Child.class)) {
                        unmarshallChild(entity, field, object);
                    } else if (field.isAnnotationPresent(Flat.class)) {
                    	unmarshallFlat(entity, object, field);
                    }
                } catch (InstantiationException ex) {
                    throw new CatfishException("Encountered error while unmarshalling " + object + " with key " + key + ".", ex);
                } catch (IllegalAccessException ex) {
                    throw new CatfishException("Encountered error while unmarshalling " + object + " with key " + key+ ".", ex);
                } finally {
                    field.setAccessible(accessable);
                }
            }

        } while ((cls = cls.getSuperclass()) != null);

        if (object.getClass().isAnnotationPresent(entity())) {
            Key key = entity.getKey();
            objects.put(key, object);
            entities.put(key, entity);
        }

    }

    private void unmarshallFlat(Entity entity, Object object, Field field) throws IllegalArgumentException,
            IllegalAccessException {
        Object flat = catfish.instantiate(field.getType());
        doUnmarshall(flat, entity);

        if  (Modifier.isFinal(field.getModifiers())) {
            throw new CatfishException(field + " is marked final.");        	
        } else {
        	field.set(object, flat);
        }

    }

    private void unmarshallKey(Entity entity, Object object, Field field) throws IllegalAccessException {
        Key key = entity.getKey();
        setField(object, field, key);
    }

    private void unmarshallChild(Entity entity, Field field, Object object) throws IllegalAccessException,
            IllegalArgumentException, InstantiationException {
        if (Collection.class.isAssignableFrom(field.getType())) {
            unmarshallChildCollection(entity, field, object);
        } else {
            unmarshallSingleChild(entity, field, object);
        }
    }

    private void unmarshallSingleChild(Entity entity, Field field, Object object) throws IllegalAccessException {
        Key key;
        Object child;
        String propertyName;

        propertyName = field.getAnnotation(Child.class).name().trim();

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        try {

            key = (Key) entity.getProperty(propertyName);

            if (key == null) {
                field.set(object, null);
                return;
            }

        } catch (ClassCastException ex) {
            Object tmp = entity.getProperty(propertyName);
            Class<?> cls = tmp == null ? null : tmp.getClass();
            throw new CatfishException("Expected " + Key.class + " but got " + cls + " for property " + propertyName);
        }

        child = objects.get(key);

        if (catfish.isCachingEnabled() && child == null) {

            child = memcache.get(key);

            if (child != null) {
                catfish.inject(child);
                logger.finer("Memcache hit for object: " + key);
            } else {
                logger.finer("Memcache miss for object: " + key);
            }

        }

        if (child == null) {
            Entity childEntity;
            child = catfish.instantiate(key);
            try {

                childEntity = catfish.getDataSource().get(transaction, key);

                doUnmarshall(child, childEntity);

            } catch (EntityNotFoundException ex) {
                child = null;
            }
        }

        setField(object, field, child);

    }

    @SuppressWarnings("unchecked")
    private void unmarshallChildCollection(Entity entity, Field field, Object object) throws IllegalArgumentException,
            IllegalAccessException, InstantiationException {
        String propertyName;
        Collection<?> children;
        Collection<Object> value;

        propertyName = field.getAnnotation(Child.class).name().trim();

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        try {

            children = (Collection<?>) entity.getProperty(propertyName);

            if (children == null) {

            	if (!Modifier.isFinal(field.getModifiers())) {
            		field.set(object, null);            		
            	} else {
            		throw new CatfishException(field + " is marked final.");
            	}

                return;
            }

        } catch (ClassCastException ex) {
            Object tmp = entity.getProperty(propertyName);
            Class<?> cls = tmp == null ? null : tmp.getClass();
            throw new CatfishException("Expected instance of" + Collection.class + " but got " + cls + " for property "
                    + propertyName);
        }

        if (Collection.class.isAssignableFrom(field.getType())) {
            value = (Collection<Object>) field.get(object);
        } else {
            throw new CatfishException("Field is compatible with type " + Collection.class + " but rather "
                    + field.getType() + ".");
        }

        if (value == null) {
            value = (Collection<Object>) field.getType().newInstance();

            if (!Modifier.isFinal(field.getModifiers())) {
                field.set(object, value);            	
            } else {
            	throw new CatfishException(field + " is marked final.");
            }

        } else {
            try {
                value.clear();
            } catch (UnsupportedOperationException ex) {
                String message = "Could not clear existing collection " + value + " for set of new values.";
                throw new CatfishException(message, ex);
            }
        }

        for (Object obj : children) {
            Key key;
            Object child;

            if (obj != null && !(obj instanceof Key)) {
                throw new CatfishException("Expected Key in Collection but got " + obj.getClass() + " for field "
                        + field);
            }

            try {
                key = (Key) obj;
            } catch (ClassCastException ex) {
                throw new CatfishException("Expected Key but got " + obj + " instead.", ex);
            }

            child = objects.get(key);

            if (catfish.isCachingEnabled() && child == null) {

                child = memcache.get(key);

                if (child != null) {
                    catfish.inject(child);
                    logger.finer("Memcache hit for object: " + key);
                } else {
                    logger.finer("Memcache miss for object: " + key);
                }

            }

            if (child == null) {
                try {
                    Entity childEntity = catfish.getDataSource().get(transaction, key);
                    child = catfish.instantiate(key);
                    doUnmarshall(child, childEntity);
                } catch (EntityNotFoundException ex) {
                    continue;
                }
            }

            value.add(child);

        }

    }

    private void unmarshallProperty(Entity entity, Object object, Field field) throws IllegalAccessException,
            IllegalArgumentException, InstantiationException {
        if (Collection.class.isAssignableFrom(field.getType())) {
            unmarshallCollectionProperty(entity, object, field);
        } else {
            unmarshallSingleProperty(entity, object, field);
        }
    }

    @SuppressWarnings("unchecked")
    private Object provideValue(Field field) {
        ValueProvider<Object> provider;
        String name = field.getAnnotation(Property.class).provider();

        provider = (ValueProvider<Object>) valueProviders.get(name);

        if (provider == null) {
            throw new CatfishException("Provider specified on field " + field + " named " + name + " does not exist.");
        }

        try {
            return provider.provide(field.getType());
        } catch (ClassCastException ex) {
            throw new CatfishException("ValueProvider " + name + " incorrectly configured for field " + field + ".", ex);
        }

    }

    private void setField(Object object, Field field, Object value) throws IllegalAccessException {
        try {
            Class<?> type = field.getType();

            if (value instanceof Long) {
                Long lvalue = (Long) value;

                if (type.equals(Short.class)) {
                    value = new Short(lvalue.shortValue());
                } else if (type.equals(Integer.class)) {
                    value = new Integer(lvalue.intValue());
                } else if (type.equals(short.class)) {
                    field.setShort(object, lvalue.shortValue());
                    return;
                } else if (type.equals(int.class)) {
                    field.setInt(object, lvalue.intValue());
                    return;
                }

            } else if (value instanceof Double) {
                Double dvalue = (Double) value;

                if (type.equals(Float.class)) {
                    value = dvalue.floatValue();
                } else if (type.equals(float.class)) {
                    field.setFloat(object, dvalue.floatValue());
                    return;
                }

            }

            if (field.get(object) != value) {
            	if (!Modifier.isFinal(field.getModifiers())) {
                    field.set(object, value);            		
            	}  else {
                	throw new CatfishException(field + " is marked final.");
                }
            }

        } catch (IllegalArgumentException ex) {
            Class<?> cls = value == null ? null : value.getClass();
            throw new CatfishException("Expected " + field.getType() + " but got " + cls + " for field " + field);
        }
    }

    @SuppressWarnings("unchecked")
    private Object convert(Field field, Object object, Object value, Class<?> type) throws IllegalAccessException {
        Converter<Object, Object> converter;
        String name = field.getAnnotation(Property.class).converter();

        try {

            if (name.isEmpty()) {
                return value;
            } else if ((converter = (Converter<Object, Object>) converters.get(name)) == null) {
                throw new CatfishException("Converter \"" + name + "\" not found.");
            }

            return converter.load((Class<Object>) type, field.get(object), value);
        } catch (ClassCastException ex) {
            throw new CatfishException("Type mismatch for converter \"" + name + "\".", ex);
        }

    }

    private Object convert(Field field, Object object, Object value) throws IllegalAccessException {
        return convert(field, object, value, field.getType());
    }

    private void unmarshallSingleProperty(Entity entity, Object object, Field field) throws IllegalAccessException {
        Object value;
        String propertyName;

        propertyName = field.getAnnotation(Property.class).name().trim();

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        if (entity.hasProperty(propertyName)) {
            value = entity.getProperty(propertyName);
            value = convert(field, object, value);
        } else {
            value = provideValue(field);
        }

        if (!Modifier.isFinal(field.getModifiers())) {
            setField(object, field, value);            
        }

    }

    @SuppressWarnings("unchecked")
    private void unmarshallCollectionProperty(Entity entity, Object object, Field field) throws IllegalAccessException,
            InstantiationException {
        String name;
        Object value;
        String propertyName;
        Collection<Object> collection;
        Converter<Object, Object> converter;

        name = field.getAnnotation(Property.class).converter();
        propertyName = field.getAnnotation(Property.class).name().trim();

        if (!name.isEmpty() && !converters.containsKey(name)) {
            throw new CatfishException("Converter \"" + name + "\" not found.");
        } else {
            converter = (Converter<Object, Object>) converters.get(name);
        }

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        if (entity.hasProperty(propertyName)) {
            value = entity.getProperty(propertyName);
        } else {
            value = provideValue(field);
            setField(object, field, value);
            return;
        }

        if ((value != null) && !(value instanceof Collection<?>)) {
            throw new CatfishException("Expected Collction<?>  but got " + value + " for property " + field
                    + " instead.");
        }

        if ((collection = (Collection<Object>) field.get(object)) == null) {
            collection = (Collection<Object>) field.getType().newInstance();
            
            if (!Modifier.isFinal(field.getModifiers())) {
            	field.set(object, collection);	
            } else {
            	throw new CatfishException(field + " is marked final.");
            }

        } else {
            try {
                collection.clear();
            } catch (UnsupportedOperationException ex) {
                throw new CatfishException("Could not clear existing collection " + collection
                        + " for set of new values.", ex);
            }
        }

        if (value == null) {
            return;
        }

        if (converter == null) {
            for (Object element : (Collection<?>) value) {
                collection.add(element);
            }
        } else {
            Type type = field.getGenericType();

            if (type instanceof ParameterizedType) {
                Type[] types = ((ParameterizedType) type).getActualTypeArguments();

                if (types.length == 1) {
                    type = types[0];
                } else {
                    type = Unknown.class;
                }

            }
    
            for (Object element : (Collection<?>) value) {
                element = converter.load(type, null, element);
                collection.add(element);
            }
        }

    }
}
